Skia parity: premultiplied rendering, layer effects, and WebGPU interop rework - #410
Merged
Conversation
Introduces a new clipping model that separates clip operations from state saving. Adds DrawingClipState, DrawingClipDescriptor, DrawingClipKind, and DrawingClipEdgeMode to manage clip primitives. Updates Save()/Restore() to work with explicit Clip() calls instead of clip parameters. Extends WebGPU shader pipeline to support difference clipping alongside intersection clips, with proper stack handling in clip_leaf and clip_reduce shaders. Updates samples and tests to use the new API pattern.
Simplifies drawing and clipping configuration by deleting `ShapeOptions` and moving fill-rule handling to `DrawingOptions.IntersectionRule`. Clipping APIs now take `BooleanOperation` and optional `IntersectionRule` directly, and rendering paths/backends were updated to use the new model. Tests were adjusted to cover the new option flow and preserve existing clipping/text behavior.
Correct elliptic gradient rotation and add isolated clip-group handling in WebGPU so layers and clip masks compose consistently with the CPU backend. Also updates related documentation and adds a regression test for rotated elliptic gradients.
Presentation flushes now defer scratch-overflow readbacks and resolve them on later flushes, with corrective rerenders for offscreen targets and frame pacing via queue work-done signals. Text rendering also switches to position-independent glyph caching with explicit sub-pixel offsets, and projective transforms now bypass the cache.
Adds a per-pixel-type static pool for `WorkerState<TPixel>` in the default CPU drawing backend. Worker states are now rented at the start of each `Parallel.For` loop and returned instead of disposed, reusing allocator-owned buffers (brush workspace, raster scratch, clip coverage) across flushes. The pool is capped at `Environment.ProcessorCount` entries. A `Gen2TrimSentinel` finalizer trims the pool on each Gen2 GC cycle when no rent has occurred since the previous one, returning all held buffers to the memory allocator during idle periods. Includes a new test that verifies all allocator buffers are returned after rendering stops and two Gen2 collections are observed. Reference output images updated to reflect rendering improvements in the same changeset.
Reworked `BaseGlyphBuilder` decoration tracking to dedupe repeated lines from stacked color glyph layers while still stitching only between adjacent glyphs (not within skip-ink segments). This fixes underline behavior with tracking (issue #405), keeps skip-ink behavior correct, and updates expected rendering outputs. Also migrated text rendering calls to the newer `TextRenderer.Render(...)` overloads, isolated the worker-state pool idle-trim test in `RemoteExecutor` for deterministic GC behavior, and replaced a WebGPU pending-status record with an explicit readonly struct.
Introduce `DrawingOptions.TextContrast` (default `0.5`) and thread it through text command creation into `RasterizerOptions.CoverageBoost`. Apply the same S-curve coverage remap in both CPU rasterization (`AreaToCoverage`) and WebGPU fine shading for antialiased text only, while keeping fills/strokes/clips unchanged and preserving aliased-threshold behavior. Update shader comments and rasterizer/canvas docs to explain the shared coverage parameter and the curve rationale.
The rasterizer's recursive segment subdivider computed midpoints using 32-bit integer arithmetic, causing overflow for coordinates beyond ~4.2M pixels. The overflow placed the midpoint outside [x0, x1], so the segment never shrank and the recursion overflowed the stack. Fix by widening dx/dy comparisons and midpoint arithmetic to 64-bit, then casting the result back to int. Adds regression tests covering extreme float values (large positive/negative, NaN, Infinity) and updates reference images affected by the rendering change.
Update the image baselines for text, path, emoji, and font rendering tests to match the current output.
Track and propagate CPU-side tile-crossing/bin-footprint estimates through scene encoding and range metadata, then use them to seed scratch buffer capacities with device-limit clamping. Preserve backend bump sizes as a high-water mark (max-merge) instead of shrinking per scene, and start best-effort background compute pipeline warmup at device creation to reduce first-flush shader compilation stalls.
Improves WebGPU staged-scene reliability by requesting adapter storage limits at device creation and by computing the effective scratch ceiling from both max storage binding size and max buffer size. Adds full deferred chunked overflow readback support, including variable-sized readback buffer pooling, chunk-aware pending status resolution, and shared chunk status aggregation logic. Tightens scene scratch seeding with per-segment tile-crossing estimates (fills and strokes) to avoid over/under-seeding, and adds diagnostics regression tests for black-screen, chunking behavior, and first-pass convergence.
Document why non-zero polygons must be normalized before clipping with the even-odd clipper, and remove an unused using from `ClippedShapeGenerator`.
Makes several WebGPU and drawing backend types/methods public for external use, and refreshes the related XML docs. Also fixes deferred readback buffer recycling and evicts cached device state during device recovery to avoid leaking pipelines and native device references.
Move from creating separate WebGPU instances per surface to a process-shared instance managed by the runtime. This fixes corruption of wgpu-native's global backend registry that occurs during rapid window reopens, which would leave freshly created surfaces invalid and cause process aborts. Surfaces now borrow the shared instance via non-owning handles.
Replaces the previous stitch-and-suppress decoration approach with a two-glyph sliding window that carves skip-ink gaps around actual ink intervals (widened by the drawn thickness) and emits the surviving segments. Key changes: - `BaseGlyphBuilder` now receives ink intersection intervals from the font engine and uses a `DecorationLane`/`PendingDecoration` window to carve each glyph against its neighbours. - `RichTextRun.GetDecorationOptions` reports the pen's stroke width as the decoration thickness so skip-ink gaps are sized to match what is actually painted. - `RichTextGlyphRenderer.SetDecoration` simplified: path already carries the correct dimensions; pen rescaling and anchor logic removed. - `DrawTextOperations` / `CreateTextCompositionCommand` drop the redundant `clipState` parameter. - `DefaultTextContrast` raised from 0.1 to 0.5. - New test `DrawTextUnderlinePenSkipsInk` verifies that a thick pen underline is still broken by descender ink.
Introduces a new `LayerEffect`/`BackdropLayerEffect` model and new `DrawingCanvas.SaveLayer` overloads to apply blur, shadow, glow, inner-shadow, color-matrix, and CSS-style backdrop filters over rectangle or path regions. `Apply` commands now support explicit write-back compositing options and offsets, with backend scene/apply plumbing updated so reads come from the pre-offset region while writes land at the offset target. WebGPU composition was also corrected to store clip stack blend snapshots in straight alpha before packing, improving low-alpha color fidelity and matching CPU round-trips. Adds extensive WebGPU parity tests, a new balloon input image, and updated/generated reference outputs.
Fix CreateApplyRasterizerOptions to properly handle transformed path bounds. When a transform is applied to the rasterization options, the interest rectangle must be calculated from the transformed bounds, not the untransformed bounds. This ensures coverage is correctly cropped for transformed paths.
Replace the Silk.NET WebGPU bindings with checked-in wgpu-native bindings and packaged RID-specific native libraries. Add shared external-surface sessions and persistent presentation targets with direct-copy and render-pipeline fallback paths across supported native window hosts. Carry alpha representation and numeric encoding through canvas creation, shaders, copies, readback, and presentation. Implement CSS Color 4 premultiplied interpolation for CPU and WebGPU gradients, including path gradients and half-float targets. Expand callback lifetime, surface, format, gradient, and CPU/WebGPU parity coverage, and update the verified rendering references.
Introduces a full WebGPU shader-effect pipeline (custom WGSL programs, uniforms/layouts, pass sequencing, diagnostics, compilation errors, precompile support, and CPU fallbacks) plus new built-in WebGPU layer/backdrop effects (blur, color matrix, drop shadow, glow, inner shadow, acrylic). Refactors backend/runtime internals to explicit WGPU native types, adds native library resolution, error-scope callbacks, effect pipeline caching, pooled textures/buffers, and ordered-scene shader-effect execution support. Updates WebGPU samples to a shared surface session architecture with scene-owned views, adds a new custom shader demo scene, refreshes window demo rich text rendering, and expands tests/reference outputs including shader source validation coverage.
Refactors text-along-path decoration rendering to accumulate contiguous same-styled glyph cells into a single segment rather than emitting one rectangle per glyph. Path-following decorations are now emitted as sampled curve bands instead of per-glyph rotated rectangles, eliminating double anti-aliased edges at cell boundaries. - Moves path-transform logic from `RichTextGlyphRenderer`/`PathGlyphBuilder` into `BaseGlyphBuilder` via a new `TextPath` property and `ApplyPathTransform` method - Deletes `PathGlyphBuilder` (replaced by a new `GlyphBuilder(IPath)` constructor) - Adds `PendingSegment` accumulator to `DecorationLane` with contiguity merging - Adds `EmitPathDecorationBand` for curve-sampled decoration geometry - Extracts `RegisterDecorationPath` to share path registration between straight and curved emitters - Adds `DrawTextDecorated` benchmark and `RichTextGlyphRendererTests` unit tests - Updates reference image for path-text triangle test
Adds translation-only matrix detection and uses it to derive visible text bounds from canvas state, enabling safe line culling for text while correctly disabling the fast path for non-translation transforms. Text composition commands now carry a dedicated sub-pixel offset (instead of embedding it into per-glyph drawing options transforms), and backend prep paths apply that offset consistently. The batcher also avoids rebuilding scene commands when brush transforms are effectively no-ops. New text tests cover offscreen culling, translated and rotated transforms, and text-block rendering parity against unculled output.
Introduce a `Reach` contract on `LayerEffect` and implement effect-specific reach values (blur, glow, shadows, color matrix, backdrop) so layer processing can reserve enough pixels for blur spread and offsets. `DrawingCanvas.SaveLayer` now inflates content/region bounds by effect reach to prevent clipped output, and a new SaveLayer blur test covers the box-shadow-style case where output must extend and feather beyond the original content bounds.
# Conflicts:
# src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs
Reworked text rendering entry points to use positioned glyph spans (`glyphIds` + `points`) instead of `GlyphRun` in `DrawingCanvas` and `TextBuilder`, and updated call sites/tests accordingly. Image drawing was also tightened by moving clipping responsibility to callers so `DrawImageCore` now assumes pre-clipped input, reducing duplicate clipping paths and keeping rect mapping consistent. The changes also include small WebGPU/sample cleanup and analyzer-driven modernizations (`_ =` discard assignments, target-typed collection literals, simplified lambdas), plus package version bumps and refreshed image baselines impacted by text output changes.
Resolve the DirectX Shader Compiler through the host's native-library search and recover its location from the loaded module, replacing the application-directory probe that failed under the NuGet runtimes layout. Anchor fallback probing to the library's own image so Native AOT shared libraries hosted by foreign processes resolve wgpu_native and DXC, and register the Silk.NET window platforms explicitly so trimming cannot remove platform discovery.
The compute-pipeline probe passed on adapters whose submission or render path later fails natively, letting GPU-less CI hosts crash the test process inside the queue submit. Extend the isolated probe to prove an indexed submission with buffer readback and a layered retained-scene render, and add a ForceFallbackAdapter environment option so software adapters such as WARP can be selected deliberately for diagnosis.
Split the isolated availability probe into staged checks: pipeline creation, an indexed submission with buffer readback, one production compute dispatch, and a layered retained-scene render. Distinct exit codes name the first failing stage, and codes reserved for completed probes map to the unsupported result instead of a process failure. Adapters whose driver stack wedges after real render work, such as WARP, now classify as unsupported before any test or consumer can reach the native fault.
Rerun the WebGPU suite on the Windows job with the support probe bypassed and minidump collection enabled, uploading any crash dump as an artifact. The step never affects job status and exists to capture the WARP-only native fault with symbols for upstream diagnosis.
An exception during an ordered flush skipped copying the executor's replacement arenas back to the caller, so the finally returned already-disposed arenas to the reuse caches; a later drain then released their buffers a second time, corrupting native memory. Arenas now copy back unconditionally, and both arena types dispose behind an interlocked flag, nulling each buffer so a stale reference fails CanReuse instead of touching freed memory. Disposal is hardened across both projects: guard flags set before releases, exception-safe multi-buffer creation, per-entry isolation in the deferred status harvest, retained-scene item release on the empty and exception paths of scene creation, root/region canvases sharing one deferred-image list, and decoration ink buffers owned by the glyph builder base with a chained virtual dispose. Software adapters are rejected at adapter selection via wgpuAdapterGetInfo; GPU-less environments are served by the CPU backend. The staged submission, dispatch, and scene-render probes and the ForceFallbackAdapter option are removed along with the CI diagnostic capture.
WGSL leaves atan2 implementation-defined when x is zero, and axis-aligned caps and joins produce exactly those inputs; an adapter returning an indeterminate value there collapsed the sweep and dropped entire round-cap arcs. Cap and join arcs now derive their sweep from atan2(cross, dot), which stays away from the poles for every non-degenerate offset pair, and generate interior points by rotating the start offset instead of evaluating absolute angles.
The reference images are rendered on one adapter; conforming adapters differ by one LSB across a small fraction of pixels, and by antialiased edge coverage on region boundaries. The measured differences are 0.003-0.010 percent under the comparer's metric, against thresholds of 0.0005-0.0006; the new values carry roughly double the measured headroom while staying far below the 0.39 percent a single-LSB difference on every pixel would produce.
Each threshold is the worst measured cross-adapter difference at its call site, plus one at the measured precision: 0.0042 to 0.0043 for the backdrop filters, 0.0044 to 0.0045 for the many-layer clip reduce, and 0.0097 to 0.0098 for the nested-region edge coverage.
Coverage previously excluded the entire WebGPU assembly via a temporary runsettings filter; the assembly is now measured, with generated interop bindings and embedded shader text excluded as non-executable. New tests pin the Region rect-set contract (previously untested), the DrawingClipState stack including inline-slot overflow, translation, transformation and bounds queries, and completeness of the environment error messages, which caught SoftwareAdapterUnsupported mapping to the unknown-error fallback.
The shared-infrastructure guard sources are covered by the submodule's own tests and are excluded from this repository's metric. New tests cover the Brushes pattern factories in both overload forms, the LinearGeometry polyline surface including segment enumeration, lengths, shoelace areas, winding containment, distance queries and sub-path extraction, path normalization including the positive-winding lobe selection for self-intersecting contours, the native-to-public error type mapping, the window option defaults, the shader diagnostic and compilation exception surface, and the native type name attribute.
4 tasks
This was
linked to
issues
Jul 31, 2026
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Prerequisites
Description
This PR takes the drawing pipeline to output parity with Skia across the CPU and WebGPU
backends. It lands associated-alpha (premultiplied) rendering end to end, a Skia-style
layer effect system, a reworked clipping model, substantial text rendering improvements,
a rebuilt native WebGPU interop layer, and a long list of rendering fixes, with
equivalence tests and golden comparisons holding both backends to matching output.
56 commits; ~330 source files (+53k/−7k) and ~6k lines of new test coverage.
Pipeline and backends
normalized pre-transformed brushes, pooled worker state across parallel flushes, and
rectangular clip fast paths, with the pipeline documentation expanded to match.
WebGPU backend
wgpubindings (headers,generator scripts, and pinned native loaders included), with a WGSL shader pipeline and
a scene encoder with monoid-based draw ordering.
IWebGPUShaderEffect), external surface and texture interop(
GpuImageSource, surface alpha modes, shared WebGPU instance across surfaces, macOSMetal layer factory), and premultiplied interop with native surfaces.
buffers recycle correctly, and device recovery evicts cached pipeline and device state.
command chunking with status handling, pipeline warmup, and tile-boundary winding fixes.
runtimes/layout, self-contained publishes, and Native AOT — including AOT sharedlibraries, where probing anchors to the library's own image. The DirectX Shader Compiler
path is resolved through the host's native-library search rather than directory
conventions, and window platform registration is explicit so trimming cannot remove it.
Clipping
ClipOperationsupport (intersectionand difference), region-path clipping, rectangular fast paths, and hard versus
antialiased clip edges. Clips are pre-transformed and normalized once, then shared by
both backends.
Layers and effects
SaveLayergains effect-carrying overloads: bounds or a path plus a restore-timeLayerEffect— blur, drop shadow, glow, inner shadow, and color matrix.BackdropLayerEffectfamily covers the CSS backdrop-filter set (blur,brightness, contrast, color matrix, drop shadow, grayscale, hue rotate, invert, opacity,
saturate, sepia) plus acrylic.
allocate exactly what the effect reads.
Associated (premultiplied) alpha
asserting that straight and associated canvases composite to identical results across
fills, gradients, images, text, clips, layers, and effects.
pixel encoders shared across gradient brushes.
Brushes and images
modes (
None,Repeat,Mirror,Clamp).DrawImageconverts only the clipped source region for foreign pixel formats and clipsonce at the public entry points; the core assumes pre-clipped input, keeping rectangle
mapping bit-identical across overloads.
Text
DrawingTextCacheserves all canvases; glyph runs are culled and batched bysubpixel phase.
TextContrastcoverage boost, and a reworked glyph drawing API (
RichGlyphOptions) with correcteddecoration transforms across layered and path text.
Geometry and correctness fixes
TryGetPathPointAtDistance,polygon clip normalization, elliptic gradient fixes, and a stack overflow fix for paths
with huge coordinates StackOverflow in DefaultRasterizer.AddContainedLineF24Dot8 when invoking DrawLine #403.
Performance
FillParis(the 30k-path Paris street map benchmark from the 3.0 announcement, 1096×1060,fill-only with per-path transforms and opacity), re-measured on this branch. Environment:
AMD Ryzen AI Max+ 395 with Radeon 8060S, Windows 11, .NET 10.0, BenchmarkDotNet 0.15.8,
Server GC, 3 launches × 40 iterations, SkiaSharp as baseline.
¹ Median; one launch of three degraded (mean 57.2 ms, StdDev 22.7 ms), ratio computed
from the median.
Against the published 3.0 numbers on identical hardware, this branch improves every
ImageSharp result: immediate CPU 48.5 → 43.0 ms, retained CPU 17.9 → 11.9 ms, immediate
WebGPU 24.8 → 20.0 ms, with retained WebGPU holding at ~5.5 ms — 2.4x faster than
SkiaSharp on the CPU, 18x faster on the GPU with a retained scene.
Breaking changes
ShapeOptionsis removed from the drawing APIs;IntersectionRulelives onDrawingOptions.ClipOperation.Testing
and effect coverage, DrawImage no-op and disposal cases, and WebGPU golden comparisons.
Text rendering reference images are refreshed to match the corrected output.